﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UMLSynthesizer.LangAnalyzer;
using UMLSynthesizer.LangAnalyzer.Elements;
using UMLSynthesizer.LangAnalyzer.ExtensionMethods;
using UMLSynthesizer.Tasks.DiagramElements;
using UMLSynthesizer.Tasks.DiagramElements.Components;
using UMLSynthesizer.Tasks.DiagramElements.Links;
using UMLSynthesizer.Tasks.DiagramElements.Objects;
using UMLSynthesizer.Tasks.View;
using UMLSynthesizer.Tasks.ViewModel;

namespace UMLSynthesizer.Tasks
{
    public class ClassDiagramTask : ITaskBase
    {
        #region Declarations

        public const string DisplayName = "Create Class diagram with Metrics";

        #endregion

        #region Fields

        Action notifyRefresh;
        ClassDiagramControlPanelViewModel CDControlPanelViewModel;

        #endregion

        #region Public Interface

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="notifyRefresh">
        /// The delegate method to call when a refresh of picture is needed
        /// </param>
        public ClassDiagramTask(Action notifyRefresh)
        {
            this.notifyRefresh = notifyRefresh;
        }

        public void StartTask(IEnumerable<string> parameters = null)
        {
            //Select the project
            SelectProjectTask projectSelecterTask = new SelectProjectTask();
            projectSelecterTask.StartTask();

            switch (projectSelecterTask.CurrentState)
            {
                case SelectProjectTask.SelectionTaskState.ProjectSelected:
                    break;
                case SelectProjectTask.SelectionTaskState.Aborted:
                    return;
                case SelectProjectTask.SelectionTaskState.Error_NoAnalyzer:
                    throw new InvalidOperationException("Selected language has not an analyzer");
                case SelectProjectTask.SelectionTaskState.NoCodeSelected:
                    System.Windows.MessageBox.Show("You didn't select any file");
                    return;
                default:
                    throw new InvalidOperationException("The previous task is not terminated");
            }

            //Prepare View and ViewModel
            var CDControlPanelWindow = new ClassDiagramControlPanelWindow();
            CDControlPanelViewModel = new ClassDiagramControlPanelViewModel(
                projectSelecterTask.SelectedProject, notifyRefresh);

            EventHandler handler = null;
            handler = delegate
            {
                CDControlPanelViewModel.RequestClose -= handler;
                CDControlPanelWindow.Close();
            };
            CDControlPanelViewModel.RequestClose += handler;

            //Show dialog
            CDControlPanelWindow.DataContext = CDControlPanelViewModel;
            CDControlPanelWindow.Show();
        }

        #endregion

        #region Properties

        public IEnumerable<DiagramElementBase> ElementsToPrint
        {
            get
            {
                if (CDControlPanelViewModel == null ||
                    CDControlPanelViewModel.SelectedElement == null)
                    return new List<DiagramElementBase>();

                return BuildElementsToPrint(CDControlPanelViewModel.SelectedElement,
                    new Dictionary<IElementBase, DiagramObjectBase>());
            }
        }

        #endregion

        #region Private helpers

        IEnumerable<DiagramElementBase> BuildElementsToPrint(IElementBase element,
            Dictionary<IElementBase, DiagramObjectBase> elementDictionary)
        {
            List<DiagramElementBase> elementsToPrint = new List<DiagramElementBase>();

            switch (element.Kind)
            {
                case ElementKinds.Class:
                    //Add object
                    ClassObject classObject = new ClassObject(element.Identifier)
                    {
                        IsAbstract = (element as IClassElement).IsAbstract,
                        Visibility = ConvertVisibility((element as IClassElement).Visibility)
                    };

                    //Add attributes, methods and properties as methods
                    if (CDControlPanelViewModel.ShowClassMembers)
                    {
                        classObject.Attributes = ExtractAttributes(element as IClassElement,
                            CDControlPanelViewModel.ShowClassTypes);
                        classObject.Methods = ExtractMethods(element as IClassElement,
                            CDControlPanelViewModel.ShowClassTypes,
                            CDControlPanelViewModel.ShowClassMethodParameters);
                    }

                    elementsToPrint.Add(classObject);
                    elementDictionary.Add(element, classObject);

                    //Add metric
                    if (CDControlPanelViewModel.ShowClassMetrics)
                        classObject.Note = CDControlPanelViewModel.SelectedClassMetric.ToString() + ": " +
                            RoundDecimal(LanguageAnalyzer.CalculateMetric(element, CDControlPanelViewModel.SelectedClassMetric), 2);
                    break;

                case ElementKinds.Interface:
                    //Add object
                    InterfaceObject interfaceObject = new InterfaceObject(element.Identifier)
                    { Visibility = ConvertVisibility((element as IInterfaceElement).Visibility) };

                    //Add methods and properties as methods
                    if (CDControlPanelViewModel.ShowInterfaceMembers)
                        interfaceObject.Methods = ExtractMethods(element as IInterfaceElement,
                            CDControlPanelViewModel.ShowInterfaceTypes,
                            CDControlPanelViewModel.ShowInterfaceMethodParameters);

                    elementsToPrint.Add(interfaceObject);
                    elementDictionary.Add(element, interfaceObject);

                    //Add metric
                    if (CDControlPanelViewModel.ShowInterfaceMetrics)
                        interfaceObject.Note = CDControlPanelViewModel.SelectedInterfaceMetric.ToString() + ": " +
                            RoundDecimal(LanguageAnalyzer.CalculateMetric(element,
                            CDControlPanelViewModel.SelectedInterfaceMetric), 2);
                    break;

                case ElementKinds.Namespace:
                case ElementKinds.Project:
                    //Add object
                    PackageObject packageObject = new PackageObject(element.Identifier);

                    //Populate child elements
                    Dictionary<IElementBase, DiagramObjectBase>
                        packageElementDictionary = new Dictionary<IElementBase, DiagramObjectBase>(),
                        packageExternalElementDictionary = new Dictionary<IElementBase, DiagramObjectBase>();

                    var childElements = new List<DiagramElementBase>(
                        from DiagramObjectBase child in
                            //Select classes
                            (element as IContainerOfElementsBase).ContainedClasses
                            .Where(param => CDControlPanelViewModel.ShowContainedClasses)
                            .SelectMany(containedClass => BuildElementsToPrint(containedClass, packageElementDictionary))
                            .Union(
                            //Select interfaces
                            (element as IContainerOfElementsBase).ContainedInterfaces
                            .Where(param => CDControlPanelViewModel.ShowContainedInterfaces)
                            .SelectMany(containedInterface => BuildElementsToPrint(containedInterface, packageElementDictionary)))
                            .Union(
                            //Select packages
                            (element as IContainerOfElementsBase).ContainedNamespaces
                            .Where(param => CDControlPanelViewModel.ShowContainedPackages)
                            .SelectMany(containedPackage => BuildElementsToPrint(containedPackage, packageElementDictionary)))
                            .Union(
                            //Select structs
                            (element as IContainerOfElementsBase).ContainedStructs
                            .Where(param => CDControlPanelViewModel.ShowContainedStructs)
                            .SelectMany(containedStruct => BuildElementsToPrint(containedStruct, packageElementDictionary)))
                        orderby child.Identifier
                        select child);

                    //Add relations between elements
                    foreach (var keyValueElement in (from e in packageElementDictionary
                                                     where e.Value.Kind == DiagramElementBase.ElementKinds.Class ||
                                                     e.Value.Kind == DiagramElementBase.ElementKinds.Interface
                                                     select e).ToArray())
                    {
                        //Select dependencies
                        //If there is a relation for inheritage between two classes, don't show the dependency relation
                        var inheritanceElements = (keyValueElement.Key as IElementWithHeritageBase != null ?
                            (keyValueElement.Key as IElementWithHeritageBase).BaseList
                            .Where(e => e != keyValueElement.Key) : new IElementWithHeritageBase[0]);
                        var dependenceElements = keyValueElement.Key.GetUsedTypes()
                            .Select(e => (e as IArrayType != null ? (e as IArrayType).BaseType : e)) //take base type from arrays
                            .Except(inheritanceElements)
                            .Where(e => e != keyValueElement.Key)
                            .Where(e => e.Kind != ElementKinds.ExternalType ||
                                !(e as IExternalType).IsPrimitiveType ||
                                CDControlPanelViewModel.ShowPrimitiveTypes)
                            .Distinct();

                        foreach (var relatedElement in dependenceElements)
                        {
                            DiagramObjectBase relatedElementToPrint = SelectElementToPrint(relatedElement, element.GetDefinitionPath(), 
                                packageElementDictionary, packageExternalElementDictionary, childElements);
                            if (relatedElementToPrint != null)
                                childElements.Add(new DependencyLink(keyValueElement.Value, relatedElementToPrint));
                        }
                        foreach (var relatedElement in inheritanceElements)
                        {
                            DiagramObjectBase relatedElementToPrint = SelectElementToPrint(relatedElement, element.GetDefinitionPath(), 
                                packageElementDictionary, packageExternalElementDictionary, childElements);
                            if (relatedElementToPrint != null)
                            {
                                //if is the special case class --|> interface use Realization instead Generalization
                                if (keyValueElement.Value as ClassObject != null &&
                                    relatedElementToPrint as InterfaceObject != null)
                                    childElements.Add(new RealizationLink(keyValueElement.Value, relatedElementToPrint));
                                else
                                    childElements.Add(new GeneralizationLink(keyValueElement.Value, relatedElementToPrint));
                            }
                        }
                    }

                    packageObject.ChildElements = childElements;
                    elementsToPrint.Add(packageObject);
                    elementDictionary.Add(element, packageObject);
                    
                    //Add metric
                    if (CDControlPanelViewModel.ShowPackageMetrics)
                        packageObject.Note = CDControlPanelViewModel.SelectedPackageMetric.ToString() + ": " +
                            RoundDecimal(LanguageAnalyzer.CalculateMetric(element, CDControlPanelViewModel.SelectedPackageMetric), 2);
                    break;

                case ElementKinds.Struct:
                    //Add object
                    ClassObject structObject = new ClassObject(element.Identifier)
                    {
                        Visibility = ConvertVisibility((element as IStructElement).Visibility)
                    };

                    //Add attributes, methods and properties as methods
                    if (CDControlPanelViewModel.ShowStructMembers)
                    {
                        structObject.Attributes = ExtractAttributes(element as IStructElement,
                            CDControlPanelViewModel.ShowStructTypes);
                        structObject.Methods = ExtractMethods(element as IStructElement,
                            CDControlPanelViewModel.ShowStructTypes,
                            CDControlPanelViewModel.ShowStructMethodParameters);
                    }

                    elementsToPrint.Add(structObject);
                    elementDictionary.Add(element, structObject);

                    //Add metric
                    if (CDControlPanelViewModel.ShowStructMetrics)
                        structObject.Note = CDControlPanelViewModel.SelectedStructMetric.ToString() + ": " +
                            RoundDecimal(LanguageAnalyzer.CalculateMetric(element, CDControlPanelViewModel.SelectedStructMetric), 2);
                    break;
            }

            return elementsToPrint;
        }

        MethodComponent ConvertMethod(IMethodElement method, bool showType, bool showParameters)
        {
            MethodComponent umlMethod = new MethodComponent(method.Identifier)
            {
                IsAbstract = method.IsAbstract,
                IsStatic = method.IsStatic,
                Type = (showType ? method.Type.Identifier : null),
                Visibility = ConvertVisibility(method.Visibility)
            };
            if (showParameters)
                umlMethod.Parameters =
                    from parameter in method.ParameterList
                    select new MethodComponent.Parameter()
                    {
                        Identifier = parameter.Identifier,
                        Type = parameter.Type.Identifier
                    };
            return umlMethod;
        }
        MethodComponent ConvertPropertyAccessor(IPropertyElement property, IAccessorElement accessor, bool showType, bool showParameters)
        {
            MethodComponent umlMethod = new MethodComponent(property.Identifier + "_" + accessor.Identifier)
            {
                IsAbstract = accessor.IsAbstract,
                IsStatic = accessor.IsStatic,
                Visibility = ConvertVisibility(accessor.Visibility)
            };
            switch (accessor.Identifier)
            {
                case "get":
                    if (showType)
                        umlMethod.Type = property.Type.Identifier;
                    break;
                case "set":
                    if (showParameters)
                        umlMethod.Parameters = new MethodComponent.Parameter[]
                        {
                            new MethodComponent.Parameter()
                            {
                                Identifier = "value",
                                Type = property.Type.Identifier
                            }
                        };
                    break;
            }
            return umlMethod;
        }
        DiagramElementBase.Visibilities ConvertVisibility(VisibilityStates visibility)
        {
            switch (visibility)
            {
                case VisibilityStates.Public:
                    return DiagramElementBase.Visibilities.Public;
                case VisibilityStates.Internal:
                    return DiagramElementBase.Visibilities.Package;
                case VisibilityStates.Protected:
                    return DiagramElementBase.Visibilities.Protected;
                case VisibilityStates.Private:
                    return DiagramElementBase.Visibilities.Private;
                default:
                    return DiagramElementBase.Visibilities.ND;
            }
        }

        IEnumerable<AttributeComponent> ExtractAttributes(IContainerOfElementsBase element, bool showTypes)
        {
            return from field in element.ContainedFields
                   from variable in field.ContainedVariables
                   orderby variable.Identifier
                   select new AttributeComponent(variable.Identifier)
                   {
                       IsStatic = field.IsStatic,
                       Type = (showTypes ? field.Type.Identifier : null),
                       Visibility = ConvertVisibility(field.Visibility)
                   };
        }
        IEnumerable<MethodComponent> ExtractMethods(IContainerOfElementsBase element, bool showTypes, bool showParameters)
        {
            return from method in
                       (from method in element .ContainedMethods
                        select ConvertMethod(method, showTypes, showParameters)).Union
                       (from property in element.ContainedProperties
                        from accessor in property.Accessors
                        select ConvertPropertyAccessor(property, accessor, showTypes, showParameters))
                   orderby method.Identifier
                   select method;
        }

        /// <summary>
        /// Return the number rounded to the setted precision
        /// </summary>
        /// <param name="number">The number to round</param>
        /// <param name="precisionDigits">The precision to set</param>
        /// <returns>The rounded number</returns>
        static double RoundDecimal(double number, int precisionDigits)
        {
            for (int i = 0; i < precisionDigits; i++)
                number *= 10;
            if (number >= 0 && number % 1 >= 0.5)
                number++;
            else if (number < 0 && number % 1 <= -0.5)
                number--;
            number = (int)number;
            for (int i = 0; i < precisionDigits; i++)
                number /= 10;
            return number;
        }

        /// <summary>
        /// Select an element to print in a package or make it as extern if it doesn't exist.
        /// Return null if extern relations are hidden by Control Panel
        /// </summary>
        /// <param name="searchedElement">The element to find</param>
        /// <param name="elementDictionary">The element dictionary</param>
        /// <param name="elementsToPrint">The list of elements to print</param>
        /// <returns>The selected element. Return null if extern relations are hidden</returns>
        DiagramObjectBase SelectElementToPrint(ITypeBase searchedElement,
            string currentNamespacePath,
            Dictionary<IElementBase, DiagramObjectBase> elementDictionary,
            Dictionary<IElementBase, DiagramObjectBase> externalElementDictionary,
            List<DiagramElementBase> elementsToPrint)
        {
            DiagramObjectBase selectedElement =
                (from elementKeyValue in elementDictionary.Union(externalElementDictionary)
                 where elementKeyValue.Key == searchedElement
                 select elementKeyValue.Value).FirstOrDefault();

            //if the element doesn't exist and so it's external
            if (selectedElement == null &&
                currentNamespacePath != searchedElement.GetParentNamespace().GetDefinitionPath() &&
                CDControlPanelViewModel.ShowExternalDependencies)
            {
                selectedElement = new ExternalElementObject(searchedElement.Identifier)
                { FullPath = searchedElement.GetDefinitionPath() };
                externalElementDictionary.Add(searchedElement, selectedElement);

                //select parent package
                IContainerOfElementsBase parentPackage = searchedElement.GetParentNamespace();
                if (parentPackage == null)
                    parentPackage = searchedElement.ProjectOwner;

                //if the package exists
                if (parentPackage != null)
                {
                    //check if already exists external package
                    var externalPackageObject =
                        (from elementKeyValue in externalElementDictionary
                         where elementKeyValue.Key == parentPackage
                         select elementKeyValue.Value as ExternalPackageObject).FirstOrDefault();

                    //if the container package doesn't exist, make it
                    if (externalPackageObject == null)
                    {
                        externalPackageObject = AddExternalPackageObject(elementsToPrint, parentPackage.GetDefinitionPath());
                        externalElementDictionary.Add(parentPackage, externalPackageObject);
                    }

                    externalPackageObject.ChildElements =
                        externalPackageObject.ChildElements.Union(new DiagramElementBase[] { selectedElement });
                }
                else //if there isn't a package
                    elementsToPrint.Add(selectedElement);
            }

            return selectedElement;
        }
        /// <summary>
        /// Add hierarchically an external package object
        /// </summary>
        /// <param name="elementsToPrint">Elements to print list</param>
        /// <param name="packageDefinitionPath">The package definition path</param>
        /// <returns>The added external package object</returns>
        ExternalPackageObject AddExternalPackageObject(List<DiagramElementBase> elementsToPrint,
            string packageDefinitionPath)
        {
            //search if exists a parent package
            var parentPackage = (from element in elementsToPrint
                                 where element.Kind == DiagramElementBase.ElementKinds.ExternalPackage
                                 where packageDefinitionPath.Length > (element as ExternalPackageObject).Identifier.Length + 1 &&
                                    packageDefinitionPath.Remove((element as ExternalPackageObject).Identifier.Length + 1) ==
                                        (element as ExternalPackageObject).Identifier + "."
                                 select element as ExternalPackageObject).FirstOrDefault();

            ExternalPackageObject externalPackageObject;
            if (parentPackage != null)
            {
                var childElements = new List<DiagramElementBase>(parentPackage.ChildElements);
                externalPackageObject = AddExternalPackageObject(childElements,
                    packageDefinitionPath.Substring(parentPackage.Identifier.Length + 1));
                parentPackage.ChildElements = childElements;
            }
            else
            {
                //search if exists a child package of this
                var childPackage = (from element in elementsToPrint
                                    where element.Kind == DiagramElementBase.ElementKinds.ExternalPackage
                                    where packageDefinitionPath.Length + 1 < (element as ExternalPackageObject).Identifier.Length &&
                                       (element as ExternalPackageObject).Identifier.Remove(packageDefinitionPath.Length + 1) ==
                                           packageDefinitionPath + "."
                                    select element as ExternalPackageObject).FirstOrDefault();

                externalPackageObject = new ExternalPackageObject(packageDefinitionPath) { FullPath = packageDefinitionPath };
                if (childPackage != null)
                {
                    //replace the child with the new package
                    int childPackageIndex = elementsToPrint.IndexOf(childPackage);
                    elementsToPrint.RemoveAt(childPackageIndex);
                    elementsToPrint.Insert(childPackageIndex, externalPackageObject);

                    //rename the child package
                    childPackage.Identifier = childPackage.Identifier.Substring(externalPackageObject.Identifier.Length + 1);

                    //insert the child package into the new package
                    externalPackageObject.ChildElements = new DiagramElementBase[] { childPackage };
                }
                else
                    elementsToPrint.Add(externalPackageObject);
            }

            return externalPackageObject;
        }

        #endregion
    }
}
